/*******************************************************************************
 * This file is part of Crunch Graphics.
 *
 * Crunch Graphics is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Graphics is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Graphics.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.graphics.gpu;

import javax.media.opengl.GL;
import javax.media.opengl.GL3;

/** Encapsulates the state of the depth and stencil functionality on the GPU.
 */
public class GpuDepthStencilState extends GpuObject {
	/** Sets the state of the GPU to the state represented by this object.
	 */
	public void bind() {
		getGpuState().bind(this);
	}

	/** Sets whether the depth test is enabled.
	 */
	public void setDepthTestEnabled(boolean e) {
		depthTestEnabled = e;
	}

	/** Sets the depth function.
	 */
	public void setDepthFunction(int function) {
		depthFunction = function;
	}

	/** Sets whether depth writing is enabled.
	 */
	public void setDepthWriteEnabled(boolean e) {
		depthWriteEnabled = e;
	}

	/** Sets whether the stencil test is enabled.
	 */
	public void setStencilTestEnabled(boolean e) {
		stencilTestEnabled = e;
	}

	/** Sets the stencil function.
	 */
	public void setStencilFunction(int function) {
		stencilFunctionFront = function;
		stencilFunctionBack = function;
	}

	/** Sets the stencil function for front and back faces separately.
	 */
	public void setStencilFunctions(int functionFront, int functionBack) {
		stencilFunctionFront = functionFront;
		stencilFunctionBack = functionBack;
	}

	/** Sets the stencil reference value.
	 */
	public void setStencilReference(int reference) {
		stencilReferenceFront = reference;
		stencilReferenceBack = reference;
	}

	/** Sets the stencil reference value for front and back faces separately.
	 */
	public void setStencilReferences(int referenceFront, int referenceBack) {
		stencilReferenceFront = referenceFront;
		stencilReferenceBack = referenceBack;
	}

	/** Sets the stencil mask.
	 */
	public void setStencilMask(int mask) {
		stencilMaskFront = mask;
		stencilMaskBack = mask;
	}

	/** Sets the stencil mask for front and back faces separately.
	 */
	public void setStencilMasks(int maskFront, int maskBack) {
		stencilMaskFront = maskFront;
		stencilMaskBack = maskBack;
	}

	/** Sets the stencil operation.
	 */
	public void setStencilOperation(int operationStencilFail, int operationDepthFail, int operationDepthPass) {
		stencilOperationStencilFailFront = operationStencilFail;
		stencilOperationStencilFailBack = operationStencilFail;
		stencilOperationDepthFailFront = operationDepthFail;
		stencilOperationDepthFailBack = operationDepthFail;
		stencilOperationDepthPassFront = operationDepthPass;
		stencilOperationDepthPassBack = operationDepthPass;
	}

	/** Sets the stencil operation for front and back faces separately.
	 */
	public void setStencilOperations(
			int operationStencilFailFront, int operationDepthFailFront, int operationDepthPassFront,
			int operationStencilFailBack, int operationDepthFailBack, int operationDepthPassBack) {
		stencilOperationStencilFailFront = operationStencilFailFront;
		stencilOperationStencilFailBack = operationStencilFailBack;
		stencilOperationDepthFailFront = operationDepthFailFront;
		stencilOperationDepthFailBack = operationDepthFailBack;
		stencilOperationDepthPassFront = operationDepthPassFront;
		stencilOperationDepthPassBack = operationDepthPassBack;
	}

	/** Sets the stencil write mask.
	 */
	public void setStencilWriteMask(int mask) {
		stencilWriteMaskFront = mask;
		stencilWriteMaskBack = mask;
	}

	/** Sets the stencil write mask for front and back faces separately.
	 */
	public void setStencilWriteMasks(int maskFront, int maskBack) {
		stencilWriteMaskFront = maskFront;
		stencilWriteMaskBack = maskBack;
	}

	/** Returns whether the depth test is enabled.
	 */
	public boolean isDepthTestEnabled() {
		return depthTestEnabled;
	}

	/** Returns the depth function.
	 */
	public int getDepthFunction() {
		return depthFunction;
	}

	/** Returns whether depth writing is enabled.
	 */
	public boolean isDepthWriteEnabled() {
		return depthWriteEnabled;
	}

	/** Returns whether the stencil test is enabled.
	 */
	public boolean isStencilTestEnabled() {
		return stencilTestEnabled;
	}

	/** Returns the stencil function for front faces.
	 */
	public int getStencilFunctionFront() {
		return stencilFunctionFront;
	}

	/** Returns the stencil function for back faces.
	 */
	public int getStencilFunctionBack() {
		return stencilFunctionBack;
	}

	/** Returns the stencil reference value for front faces.
	 */
	public int getStencilReferenceFront() {
		return stencilReferenceFront;
	}

	/** Returns the stencil reference value for back faces.
	 */
	public int getStencilReferenceBack() {
		return stencilReferenceBack;
	}

	/** Returns the stencil mask for front faces.
	 */
	public int getStencilMaskFront() {
		return stencilMaskFront;
	}

	/** Returns the stencil mask for back faces.
	 */
	public int getStencilMaskBack() {
		return stencilMaskBack;
	}

	/** Returns the stencil operation "stencil fail" for front faces.
	 */
	public int getStencilOperationStencilFailFront() {
		return stencilOperationStencilFailFront;
	}

	/** Returns the stencil operation "depth fail" for front faces.
	 */
	public int getStencilOperationDepthFailFront() {
		return stencilOperationDepthFailFront;
	}

	/** Returns the stencil operation "depth pass" for front faces.
	 */
	public int getStencilOperationDepthPassFront() {
		return stencilOperationDepthPassFront;
	}

	/** Returns the stencil operation "stencil fail" for back faces.
	 */
	public int getStencilOperationStencilFailBack() {
		return stencilOperationStencilFailBack;
	}

	/** Returns the stencil operation "depth fail" for back faces.
	 */
	public int getStencilOperationDepthFailBack() {
		return stencilOperationDepthFailBack;
	}

	/** Returns the stencil operation "depth pass" for back faces.
	 */
	public int getStencilOperationDepthPassBack() {
		return stencilOperationDepthPassBack;
	}

	/** Returns the stencil write mask for front faces.
	 */
	public int getStencilWriteMaskFront() {
		return stencilWriteMaskFront;
	}

	/** Returns the stencil write mask for back faces.
	 */
	public int getStencilWriteMaskBack() {
		return stencilWriteMaskBack;
	}

	GpuDepthStencilState(GpuState state) {
		super(state);
		depthTestEnabled = false;
		depthFunction = GL.GL_LEQUAL; // note: OpenGL's default is GL.GL_LESS
		depthWriteEnabled = true;

		stencilTestEnabled = false;
		stencilFunctionFront = GL.GL_ALWAYS;
		stencilFunctionBack = GL.GL_ALWAYS;
		stencilReferenceFront = 0;
		stencilReferenceBack = 0;
		stencilMaskFront = 0xffffffff;
		stencilMaskBack = 0xffffffff;
		stencilOperationStencilFailFront = GL.GL_KEEP;
		stencilOperationStencilFailBack = GL.GL_KEEP;
		stencilOperationDepthFailFront = GL.GL_KEEP;
		stencilOperationDepthFailBack = GL.GL_KEEP;
		stencilOperationDepthPassFront = GL.GL_KEEP;
		stencilOperationDepthPassBack = GL.GL_KEEP;
		stencilWriteMaskFront = 0xffffffff;
		stencilWriteMaskBack = 0xffffffff;
	}

	/** Copy constructor.
	 */
	public GpuDepthStencilState(GpuDepthStencilState other) {
		super(other.getGpuState());
		depthTestEnabled = other.depthTestEnabled;
		depthFunction = other.depthFunction;
		depthWriteEnabled = other.depthWriteEnabled;

		stencilTestEnabled = other.stencilTestEnabled;
		stencilFunctionFront = other.stencilFunctionFront;
		stencilFunctionBack = other.stencilFunctionBack;
		stencilReferenceFront = other.stencilReferenceFront;
		stencilReferenceBack = other.stencilReferenceBack;
		stencilMaskFront = other.stencilMaskFront;
		stencilMaskBack = other.stencilMaskBack;
		stencilOperationStencilFailFront = other.stencilOperationStencilFailFront;
		stencilOperationStencilFailBack = other.stencilOperationStencilFailBack;
		stencilOperationDepthFailFront = other.stencilOperationDepthFailFront;
		stencilOperationDepthFailBack = other.stencilOperationDepthFailBack;
		stencilOperationDepthPassFront = other.stencilOperationDepthPassFront;
		stencilOperationDepthPassBack = other.stencilOperationDepthPassBack;
		stencilWriteMaskFront = other.stencilWriteMaskFront;
		stencilWriteMaskBack = other.stencilWriteMaskBack;
	}

	private boolean depthTestEnabled;   // whether the depth test is enabled
	private int depthFunction;          // function used in depth comparisons
	private boolean depthWriteEnabled;  // whether depth writing is enabled

	// whether the stencil test is enabled
	private boolean stencilTestEnabled;
	// function used in stencil comparisons for front/back facing polygons
	private int stencilFunctionFront;
	private int stencilFunctionBack;
	// the reference value used in stencil comparisons for front/back facing polygons
	private int stencilReferenceFront;
	private int stencilReferenceBack;
	// the mask used in stencil comparisons for front/back facing polygons
	private int stencilMaskFront;
	private int stencilMaskBack;
	// stencil operations for front/back facing polygons
	private int stencilOperationStencilFailFront;
	private int stencilOperationStencilFailBack;
	private int stencilOperationDepthFailFront;
	private int stencilOperationDepthFailBack;
	private int stencilOperationDepthPassFront;
	private int stencilOperationDepthPassBack;
	// stencil write mask for front facing polygons
	private int stencilWriteMaskFront;
	private int stencilWriteMaskBack;

	// compares changes against current state and apply differences
	void apply(GpuDepthStencilState currentState) {
		currentState.depthTestEnabled = setStateEnabled(depthTestEnabled, currentState.depthTestEnabled, GL.GL_DEPTH_TEST);

		if (depthTestEnabled) {
			if (depthFunction != currentState.depthFunction) {
				getGpuState().getGl().glDepthFunc(depthFunction);
				currentState.depthFunction = depthFunction;
			}
		}

		// depth write mask isn't changed using glEnable/glDisable
		if (depthWriteEnabled != currentState.depthWriteEnabled) {
			getGpuState().getGl().glDepthMask(depthWriteEnabled);
			currentState.depthWriteEnabled = depthWriteEnabled;
		}

		setStateEnabled(stencilTestEnabled, currentState.stencilTestEnabled, GL.GL_STENCIL_TEST);

		if (stencilTestEnabled) {
			if (stencilFunctionFront != currentState.stencilFunctionFront ||
					stencilReferenceFront != currentState.stencilReferenceFront ||
					stencilMaskFront != currentState.stencilMaskFront) {
				getGpuState().getGl().glStencilFuncSeparate(GL.GL_FRONT, stencilFunctionFront, stencilReferenceFront, stencilMaskFront);
				currentState.stencilFunctionFront = stencilFunctionFront;
				currentState.stencilReferenceFront = stencilReferenceFront;
				currentState.stencilMaskFront = stencilMaskFront;
			}

			if (stencilFunctionBack != currentState.stencilFunctionBack ||
					stencilReferenceBack != currentState.stencilReferenceBack ||
					stencilMaskBack != currentState.stencilMaskBack) {
				getGpuState().getGl().glStencilFuncSeparate(GL.GL_BACK, stencilFunctionBack, stencilReferenceBack, stencilMaskBack);
				currentState.stencilFunctionBack = stencilFunctionBack;
				currentState.stencilReferenceBack = stencilReferenceBack;
				currentState.stencilMaskBack = stencilMaskBack;
			}
		}

		if (stencilWriteMaskFront != currentState.stencilWriteMaskFront) {
			getGpuState().getGl().glStencilMaskSeparate(GL.GL_FRONT, stencilWriteMaskFront);
			currentState.stencilWriteMaskFront = stencilWriteMaskFront;
		}

		if (stencilWriteMaskBack != currentState.stencilWriteMaskBack) {
			getGpuState().getGl().glStencilMaskSeparate(GL.GL_BACK, stencilWriteMaskBack);
			currentState.stencilWriteMaskBack = stencilWriteMaskBack;
		}
	}

	// sets values to the current OpenGL state
	void getCurrentState() {
		depthTestEnabled = getGpuState().getGl().glIsEnabled(GL.GL_DEPTH_TEST);
		int iBuf[] = new int[1];
		byte bBuf[] = new byte[1];

		getGpuState().getGl().glGetIntegerv(GL.GL_DEPTH_FUNC, iBuf, 0);
		depthFunction = iBuf[0];

		getGpuState().getGl().glGetBooleanv(GL.GL_DEPTH_WRITEMASK, bBuf, 0);
		depthWriteEnabled = (bBuf[0] != 0);

		stencilTestEnabled = getGpuState().getGl().glIsEnabled(GL.GL_STENCIL_TEST);

		getGpuState().getGl().glGetIntegerv(GL.GL_STENCIL_FUNC, iBuf, 0);
		stencilFunctionFront = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL3.GL_STENCIL_BACK_FUNC, iBuf, 0);
		stencilFunctionBack = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_STENCIL_REF, iBuf, 0);
		stencilReferenceFront = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL3.GL_STENCIL_BACK_REF, iBuf, 0);
		stencilReferenceBack = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_STENCIL_FAIL, iBuf, 0);
		stencilOperationStencilFailFront = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL3.GL_STENCIL_BACK_FAIL, iBuf, 0);
		stencilOperationStencilFailBack = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_STENCIL_PASS_DEPTH_FAIL, iBuf, 0);
		stencilOperationDepthFailFront = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL3.GL_STENCIL_BACK_PASS_DEPTH_FAIL, iBuf, 0);
		stencilOperationDepthFailBack = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_STENCIL_PASS_DEPTH_PASS, iBuf, 0);
		stencilOperationDepthPassFront = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL3.GL_STENCIL_BACK_PASS_DEPTH_PASS, iBuf, 0);
		stencilOperationDepthPassBack = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_STENCIL_WRITEMASK, iBuf, 0);
		stencilWriteMaskFront = iBuf[0];

		getGpuState().getGl().glGetIntegerv(GL3.GL_STENCIL_BACK_WRITEMASK, iBuf, 0);
		stencilWriteMaskBack = iBuf[0];
	}
}